use dap::{DapRegistry, DebugRequest};
use futures::channel::oneshot;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task};
use gpui::{Subscription, WeakEntity};
use picker::{Picker, PickerDelegate};
use project::Project;
use rpc::proto;
use task::ZedDebugConfig;
use util::debug_panic;

use std::sync::Arc;

use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
use ui::{Context, Tooltip, prelude::*};
use ui::{ListItem, ListItemSpacing};
use workspace::{ModalView, Workspace};

use crate::debugger_panel::DebugPanel;

#[derive(Debug, Clone)]
pub(super) struct Candidate {
    pub(super) pid: u32,
    pub(super) name: SharedString,
    pub(super) command: Vec<String>,
}

pub(crate) enum ModalIntent {
    ResolveProcessId(Option<oneshot::Sender<Option<i32>>>),
    AttachToProcess(ZedDebugConfig),
}

pub(crate) struct AttachModalDelegate {
    selected_index: usize,
    matches: Vec<StringMatch>,
    placeholder_text: Arc<str>,
    pub(crate) intent: ModalIntent,
    workspace: WeakEntity<Workspace>,
    candidates: Arc<[Candidate]>,
}

impl AttachModalDelegate {
    fn new(
        workspace: WeakEntity<Workspace>,
        intent: ModalIntent,
        candidates: Arc<[Candidate]>,
    ) -> Self {
        Self {
            workspace,
            candidates,
            intent,
            selected_index: 0,
            matches: Vec::default(),
            placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
        }
    }
}

pub struct AttachModal {
    _subscription: Subscription,
    pub(crate) picker: Entity<Picker<AttachModalDelegate>>,
}

impl AttachModal {
    pub(crate) fn new(
        intent: ModalIntent,
        workspace: WeakEntity<Workspace>,
        project: Entity<Project>,
        modal: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Self {
        let processes_task = get_processes_for_project(&project, cx);

        let modal = Self::with_processes(workspace, Arc::new([]), modal, intent, window, cx);

        cx.spawn_in(window, async move |this, cx| {
            let processes = processes_task.await;
            this.update_in(cx, |modal, window, cx| {
                modal.picker.update(cx, |picker, cx| {
                    picker.delegate.candidates = processes;
                    picker.refresh(window, cx);
                });
            })?;
            anyhow::Ok(())
        })
        .detach_and_log_err(cx);

        modal
    }

    pub(super) fn with_processes(
        workspace: WeakEntity<Workspace>,
        processes: Arc<[Candidate]>,
        modal: bool,
        intent: ModalIntent,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Self {
        let picker = cx.new(|cx| {
            Picker::uniform_list(
                AttachModalDelegate::new(workspace, intent, processes),
                window,
                cx,
            )
            .modal(modal)
        });
        Self {
            _subscription: cx.subscribe(&picker, |_, _, _, cx| {
                cx.emit(DismissEvent);
            }),
            picker,
        }
    }
}

impl Render for AttachModal {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
        v_flex()
            .key_context("AttachModal")
            .track_focus(&self.focus_handle(cx))
            .w(rems(34.))
            .child(self.picker.clone())
    }
}

impl EventEmitter<DismissEvent> for AttachModal {}

impl Focusable for AttachModal {
    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
        self.picker.read(cx).focus_handle(cx)
    }
}

impl ModalView for AttachModal {}

impl PickerDelegate for AttachModalDelegate {
    type ListItem = ListItem;

    fn match_count(&self) -> usize {
        self.matches.len()
    }

    fn selected_index(&self) -> usize {
        self.selected_index
    }

    fn set_selected_index(
        &mut self,
        ix: usize,
        _window: &mut Window,
        _: &mut Context<Picker<Self>>,
    ) {
        self.selected_index = ix;
    }

    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
        self.placeholder_text.clone()
    }

    fn update_matches(
        &mut self,
        query: String,
        _window: &mut Window,
        cx: &mut Context<Picker<Self>>,
    ) -> gpui::Task<()> {
        cx.spawn(async move |this, cx| {
            let Some(processes) = this
                .read_with(cx, |this, _| this.delegate.candidates.clone())
                .ok()
            else {
                return;
            };

            let matches = fuzzy::match_strings(
                &processes
                    .iter()
                    .enumerate()
                    .map(|(id, candidate)| {
                        StringMatchCandidate::new(
                            id,
                            format!(
                                "{} {} {}",
                                candidate.command.join(" "),
                                candidate.pid,
                                candidate.name
                            )
                            .as_str(),
                        )
                    })
                    .collect::<Vec<_>>(),
                &query,
                true,
                true,
                100,
                &Default::default(),
                cx.background_executor().clone(),
            )
            .await;

            this.update(cx, |this, _| {
                let delegate = &mut this.delegate;

                delegate.matches = matches;

                if delegate.matches.is_empty() {
                    delegate.selected_index = 0;
                } else {
                    delegate.selected_index =
                        delegate.selected_index.min(delegate.matches.len() - 1);
                }
            })
            .ok();
        })
    }

    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
        let candidate = self
            .matches
            .get(self.selected_index())
            .and_then(|current_match| {
                let ix = current_match.candidate_id;
                self.candidates.get(ix)
            });

        match &mut self.intent {
            ModalIntent::ResolveProcessId(sender) => {
                cx.emit(DismissEvent);

                if let Some(sender) = sender.take() {
                    sender
                        .send(candidate.map(|candidate| candidate.pid as i32))
                        .ok();
                }
            }
            ModalIntent::AttachToProcess(definition) => {
                let Some(candidate) = candidate else {
                    return cx.emit(DismissEvent);
                };

                match &mut definition.request {
                    DebugRequest::Attach(config) => {
                        config.process_id = Some(candidate.pid);
                    }
                    DebugRequest::Launch(_) => {
                        debug_panic!("Debugger attach modal used on launch debug config");
                        return;
                    }
                }

                let workspace = self.workspace.clone();
                let Some(panel) = workspace
                    .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
                    .ok()
                    .flatten()
                else {
                    return;
                };

                let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
                    registry.adapter(&definition.adapter)
                }) else {
                    return;
                };

                let definition = definition.clone();
                cx.spawn_in(window, async move |this, cx| {
                    let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
                        return;
                    };

                    panel
                        .update_in(cx, |panel, window, cx| {
                            panel.start_session(
                                scenario,
                                Default::default(),
                                None,
                                None,
                                window,
                                cx,
                            );
                        })
                        .ok();
                    this.update(cx, |_, cx| {
                        cx.emit(DismissEvent);
                    })
                    .ok();
                })
                .detach();
            }
        }
    }

    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
        self.selected_index = 0;

        match &mut self.intent {
            ModalIntent::ResolveProcessId(sender) => {
                if let Some(sender) = sender.take() {
                    sender.send(None).ok();
                }
            }
            ModalIntent::AttachToProcess(_) => {}
        }

        cx.emit(DismissEvent);
    }

    fn render_match(
        &self,
        ix: usize,
        selected: bool,
        _window: &mut Window,
        _: &mut Context<Picker<Self>>,
    ) -> Option<Self::ListItem> {
        let hit = &self.matches.get(ix)?;
        let candidate = self.candidates.get(hit.candidate_id)?;

        Some(
            ListItem::new(SharedString::from(format!("process-entry-{ix}")))
                .inset(true)
                .spacing(ListItemSpacing::Sparse)
                .toggle_state(selected)
                .child(
                    v_flex()
                        .items_start()
                        .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
                        .child(
                            div()
                                .id(SharedString::from(format!("process-entry-{ix}-command")))
                                .tooltip(Tooltip::text(
                                    candidate
                                        .command
                                        .clone()
                                        .into_iter()
                                        .collect::<Vec<_>>()
                                        .join(" "),
                                ))
                                .child(
                                    Label::new(format!(
                                        "{} {}",
                                        candidate.name,
                                        candidate
                                            .command
                                            .clone()
                                            .into_iter()
                                            .skip(1)
                                            .collect::<Vec<_>>()
                                            .join(" ")
                                    ))
                                    .size(LabelSize::Small)
                                    .color(Color::Muted),
                                ),
                        ),
                ),
        )
    }
}

fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
    let project = project.read(cx);

    if let Some(remote_client) = project.remote_client() {
        let proto_client = remote_client.read(cx).proto_client();
        cx.background_spawn(async move {
            let response = proto_client
                .request(proto::GetProcesses {
                    project_id: proto::REMOTE_SERVER_PROJECT_ID,
                })
                .await
                .unwrap_or_else(|_| proto::GetProcessesResponse {
                    processes: Vec::new(),
                });

            let mut processes: Vec<Candidate> = response
                .processes
                .into_iter()
                .map(|p| Candidate {
                    pid: p.pid,
                    name: p.name.into(),
                    command: p.command,
                })
                .collect();

            processes.sort_by_key(|k| k.name.clone());
            Arc::from(processes.into_boxed_slice())
        })
    } else {
        let refresh_kind = RefreshKind::nothing().with_processes(
            ProcessRefreshKind::nothing()
                .without_tasks()
                .with_cmd(UpdateKind::Always),
        );
        let mut processes: Box<[_]> = System::new_with_specifics(refresh_kind)
            .processes()
            .values()
            .map(|process| {
                let name = process.name().to_string_lossy().into_owned();
                Candidate {
                    name: name.into(),
                    pid: process.pid().as_u32(),
                    command: process
                        .cmd()
                        .iter()
                        .map(|s| s.to_string_lossy().into_owned())
                        .collect::<Vec<_>>(),
                }
            })
            .collect();
        processes.sort_by_key(|k| k.name.clone());
        let processes = processes.into_iter().collect();
        Task::ready(processes)
    }
}

#[cfg(test)]
pub(crate) fn set_candidates(
    modal: &AttachModal,
    candidates: Arc<[Candidate]>,
    window: &mut Window,
    cx: &mut Context<AttachModal>,
) {
    modal.picker.update(cx, |picker, cx| {
        picker.delegate.candidates = candidates;
        picker.refresh(window, cx);
    });
}

#[cfg(test)]
pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
    modal.picker.read_with(cx, |picker, _| {
        picker
            .delegate
            .matches
            .iter()
            .map(|hit| hit.string.clone())
            .collect::<Vec<_>>()
    })
}
